/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { applyCodeAction } from '../utils/codeAction';
import { Command, CommandManager } from '../utils/commandManager';
import { ConfigurationDependentRegistration } from '../utils/dependentRegistration';
import { memoize } from '../utils/memoize';
import * as Previewer from '../utils/previewer';
import * as typeConverters from '../utils/typeConverters';
import TypingsStatus from '../utils/typingsStatus';
import FileConfigurationManager from './fileConfigurationManager';
import { snippetForFunctionCall } from '../utils/snippetForFunctionCall';
import { ServerResponse } from '../typescriptService';
import { 输入法上屏命令_ } from './cts/ctsCommands/inputMethodUpScreenCommand';

const localize = nls.loadMessageBundle();
const 输入匹配 = /(([\u4E00-\u9FA5A-Za-z_\$]*?)|([\u4E00-\u9FA5A-Za-z_\$]+?[\u4E00-\u9FA5A-Za-z0-9_\$]*?))(([a-z]+?[\+\-]*)|([a-z]+?[']*?[a-z]+?[\+\-]*?))$/
const 关键字 = new Set(["抽象","任意","转为","大数","如为","类别","常量","声明","默认","删除","运行","否则","枚举","导出","扩展","善后","循环","来自","函数","获取","如果","实现","导入","位于","推断","身为","接口","作为","键为","变量","模块","名域","新建","私有","保护","公开","只读","需要","全局","返回","设置","静态","父级","假如","符号","抛出","尝试","类型","类为","判断","外扩","选取","异步","等待","属于"])

interface DotAccessorContext {
	readonly range: vscode.Range;
	readonly text: string;
}

interface 映射<T> {
	[x: string]: T
}

export interface 库内词 {
	文本: string
	频率: number
	拼音: string
	剩余输入?: string
}

interface CompletionContext {
	readonly isNewIdentifierLocation: boolean;
	readonly isMemberCompletion: boolean;
	readonly isInValidCommitCharacterContext: boolean;
	readonly enableCallCompletions: boolean;
	readonly dotAccessorContext?: DotAccessorContext;
}

const 中文标识符缓存 = new Map<string, string>();

function 是中文标识符(s: string) {
	for (let i = 0; i < s.length; i++) {
		if (s.charCodeAt(i) > 127) {
			return true;
		}
	}
	return false;
}

class MyCompletionItem extends vscode.CompletionItem {
	public readonly useCodeSnippet: boolean;
	public readonly 是库内标识符?: boolean;
	public readonly 是中文: boolean = false;
	public readonly 是输入法结果: boolean = false;
	public readonly 是关键字结果: boolean = false;
	public readonly 上屏字符: string[] | undefined;
	public readonly 忽略上屏: boolean = false;

	constructor(
		public readonly position: vscode.Position,
		public readonly document: vscode.TextDocument,
		line: string,
		public readonly tsEntry: Proto.CompletionEntry,
		useCodeSnippetsOnMethodSuggest: boolean,
		public readonly completionContext: CompletionContext,
		public readonly metadata: any | undefined,
	) {
		super(tsEntry.name, MyCompletionItem.convertKind(tsEntry.kind));
		if (tsEntry.是库内中文结果) {
			this.是中文 = true;
			this.是输入法结果 = false;
			this.是关键字结果 = true;
			this.range = tsEntry.范围;
		}
		else if (tsEntry.是输入法结果) {
			this.是中文 = true;
			this.是输入法结果 = true;
			this.是关键字结果 = false;
			this.insertText = tsEntry.插入文本;
			this.range = tsEntry.范围;
		}
		else if (tsEntry.忽略上屏) {
			this.忽略上屏 = true;
		}

		if (tsEntry.isRecommended) {
			// Make sure isRecommended property always comes first
			// https://github.com/Microsoft/vscode/issues/40325
			this.sortText = tsEntry.sortText;
			this.preselect = true;
		} else if (tsEntry.source) {
			// De-prioritze auto-imports
			// https://github.com/Microsoft/vscode/issues/40311
			this.sortText = '\uffff' + tsEntry.sortText;
		} else {
			this.sortText = tsEntry.sortText;
		}

		this.position = position;
		this.useCodeSnippet = useCodeSnippetsOnMethodSuggest && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method);

		if (tsEntry.replacementSpan) {
			this.range = typeConverters.Range.fromTextSpan(tsEntry.replacementSpan);
			// Make sure we only replace a single line at most
			if (!this.range.isSingleLine) {
				this.range = new vscode.Range(this.range.start.line, this.range.start.character, this.range.start.line, line.length);
			}
		}

		this.insertText = tsEntry.插入文本 || tsEntry.insertText;
		this.filterText = tsEntry.过滤文本 || tsEntry.insertText;

		if (completionContext.isMemberCompletion && completionContext.dotAccessorContext) {
			this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.label);
			if (!this.range) {
				this.range = completionContext.dotAccessorContext.range;
				this.insertText = this.filterText;
			}
		}

		if (tsEntry.kindModifiers) {
			const kindModifiers = new Set(tsEntry.kindModifiers.split(/\s+/g));

			if (kindModifiers.has(PConst.KindModifiers.optional)) {
				if (!this.insertText) {
					this.insertText = this.label;
				}

				if (!this.filterText) {
					this.filterText = this.label;
				}
				this.label += '?';
			}

			if (kindModifiers.has(PConst.KindModifiers.color)) {
				this.kind = vscode.CompletionItemKind.Color;
			}

			if (tsEntry.kind === PConst.Kind.script) {
				for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) {
					if (kindModifiers.has(extModifier)) {
						if (tsEntry.name.toLowerCase().endsWith(extModifier)) {
							this.detail = tsEntry.name;
						} else {
							this.detail = tsEntry.name + extModifier;
						}
						break;
					}
				}
			}
		}
		this.resolveRange(line);
	}

	private resolveRange(line: string): void {
		if (this.range) {
			return;
		}

		const wordRange = this.document.getWordRangeAtPosition(this.position);
		if (wordRange) {
			// TODO: Reverted next line due to https://github.com/Microsoft/vscode/issues/66187
			// this.range = wordRange;
		}

		// Try getting longer, prefix based range for completions that span words
		const text = line.slice(Math.max(0, this.position.character - this.label.length), this.position.character).toLowerCase();
		const entryName = this.label.toLowerCase();
		for (let i = entryName.length; i >= 0; --i) {
			if (text.endsWith(entryName.substr(0, i)) && (!wordRange || wordRange.start.character > this.position.character - i)) {
				this.range = new vscode.Range(
					new vscode.Position(this.position.line, Math.max(0, this.position.character - i)),
					this.position);
				break;
			}
		}
	}

	private static convertKind(kind: string): vscode.CompletionItemKind {
		switch (kind) {
			case PConst.Kind.primitiveType:
			case PConst.Kind.keyword:
				return vscode.CompletionItemKind.Keyword;
			case PConst.Kind.const:
				return vscode.CompletionItemKind.Constant;
			case PConst.Kind.let:
			case PConst.Kind.variable:
			case PConst.Kind.localVariable:
			case PConst.Kind.alias:
				return vscode.CompletionItemKind.Variable;
			case PConst.Kind.memberVariable:
			case PConst.Kind.memberGetAccessor:
			case PConst.Kind.memberSetAccessor:
				return vscode.CompletionItemKind.Field;
			case PConst.Kind.function:
				return vscode.CompletionItemKind.Function;
			case PConst.Kind.memberFunction:
			case PConst.Kind.constructSignature:
			case PConst.Kind.callSignature:
			case PConst.Kind.indexSignature:
				return vscode.CompletionItemKind.Method;
			case PConst.Kind.enum:
				return vscode.CompletionItemKind.Enum;
			case PConst.Kind.module:
			case PConst.Kind.externalModuleName:
				return vscode.CompletionItemKind.Module;
			case PConst.Kind.class:
			case PConst.Kind.type:
				return vscode.CompletionItemKind.Class;
			case PConst.Kind.interface:
				return vscode.CompletionItemKind.Interface;
			case PConst.Kind.warning:
				return vscode.CompletionItemKind.Text;
			case PConst.Kind.script:
				return vscode.CompletionItemKind.File;
			case PConst.Kind.directory:
				return vscode.CompletionItemKind.Folder;
			case PConst.Kind.string:
				return vscode.CompletionItemKind.Constant;
		}
		return vscode.CompletionItemKind.Property;
	}

	@memoize
	public get commitCharacters(): string[] | undefined {
		if (this.completionContext.isNewIdentifierLocation || !this.completionContext.isInValidCommitCharacterContext) {
			if (this.tsEntry.是库内中文结果 || this.tsEntry.是输入法结果) {
				return [" ", ".", ",", ";"];
			}
			return undefined;
		}

		const commitCharacters: string[] = [];
		switch (this.tsEntry.kind) {
			case PConst.Kind.memberGetAccessor:
			case PConst.Kind.memberSetAccessor:
			case PConst.Kind.constructSignature:
			case PConst.Kind.callSignature:
			case PConst.Kind.indexSignature:
			case PConst.Kind.enum:
			case PConst.Kind.interface:
				commitCharacters.push('.', ';');
				break;
			case PConst.Kind.module:
			case PConst.Kind.alias:
			case PConst.Kind.const:
			case PConst.Kind.let:
			case PConst.Kind.variable:
			case PConst.Kind.localVariable:
			case PConst.Kind.memberVariable:
			case PConst.Kind.class:
			case PConst.Kind.function:
			case PConst.Kind.memberFunction:
			case PConst.Kind.keyword:
			case PConst.Kind.parameter:
				commitCharacters.push('.', ',', ';');
				if (this.completionContext.enableCallCompletions) {
					commitCharacters.push('(');
				}
				break;
		}
		if (this.tsEntry.是库内中文结果 || this.tsEntry.是输入法结果) {
			commitCharacters.push(" ");
		}
		return commitCharacters.length === 0 ? undefined : commitCharacters;
	}
}

class CompositeCommand implements Command {
	public static readonly ID = '_ctsscript.composite';
	public readonly ID = CompositeCommand.ID;

	public execute(...commands: vscode.Command[]) {
		for (const command of commands) {
			vscode.commands.executeCommand(command.command, ...(command.arguments || []));
		}
	}
}

class CompletionAcceptedCommand implements Command {
	public static readonly ID = '_ctsscript.onCompletionAccepted';
	public readonly ID = CompletionAcceptedCommand.ID;

	public constructor(
		private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void,
	) { }

	public execute(item: vscode.CompletionItem) {
		this.onCompletionAccepted(item);
	}
}

class ApplyCompletionCodeActionCommand implements Command {
	public static readonly ID = '_ctsscript.applyCompletionCodeAction';
	public readonly ID = ApplyCompletionCodeActionCommand.ID;

	public constructor(
		private readonly client: ITypeScriptServiceClient
	) { }

	public async execute(_file: string, codeActions: Proto.CodeAction[]): Promise<boolean> {
		if (codeActions.length === 0) {
			return true;
		}

		if (codeActions.length === 1) {
			return applyCodeAction(this.client, codeActions[0], nulToken);
		}

		interface MyQuickPickItem extends vscode.QuickPickItem {
			index: number;
		}

		const selection = await vscode.window.showQuickPick<MyQuickPickItem>(
			codeActions.map((action, i): MyQuickPickItem => ({
				label: action.description,
				description: '',
				index: i
			})), {
				placeHolder: "选择代码操作",
			}
		);
		if (!selection) {
			return false;
		}

		const action = codeActions[selection.index];
		if (!action) {
			return false;
		}
		return applyCodeAction(this.client, action, nulToken);
	}
}

interface CompletionConfiguration {
	readonly useCodeSnippetsOnMethodSuggest: boolean;
	readonly nameSuggestions: boolean;
	readonly pathSuggestions: boolean;
	readonly autoImportSuggestions: boolean;
}

namespace CompletionConfiguration {
	export const useCodeSnippetsOnMethodSuggest = 'suggest.completeFunctionCalls';
	export const nameSuggestions = 'suggest.names';
	export const pathSuggestions = 'suggest.paths';
	export const autoImportSuggestions = 'suggest.autoImports';

	export function getConfigurationForResource(
		modeId: string,
		resource: vscode.Uri
	): CompletionConfiguration {
		const config = vscode.workspace.getConfiguration(modeId, resource);
		return {
			useCodeSnippetsOnMethodSuggest: config.get<boolean>(CompletionConfiguration.useCodeSnippetsOnMethodSuggest, false),
			pathSuggestions: config.get<boolean>(CompletionConfiguration.pathSuggestions, true),
			autoImportSuggestions: config.get<boolean>(CompletionConfiguration.autoImportSuggestions, true),
			nameSuggestions: config.get<boolean>(CompletionConfiguration.nameSuggestions, true)
		};
	}
}

class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider {
	public i = 0;

	public static readonly triggerCharacters = ['.', '"', '\'', '/', '@', '<'];
	public static readonly triggerCharactersCts = ["a", "o", "e", 'i', 'u', 'v', 'b', 'p', 'm', 'f', 'd', 't', 'n', 'l', 'g', 'k', 'h', 'j', 'q', 'x', 'z', 'c', 's', 'r', 'y', 'w', '+', '-'];

	constructor(
		private readonly client: ITypeScriptServiceClient,
		private readonly modeId: string,
		private readonly typingsStatus: TypingsStatus,
		private readonly fileConfigurationManager: FileConfigurationManager,
		commandManager: CommandManager,
		onCompletionAccepted: (item: vscode.CompletionItem) => void
	) {
		commandManager.register(new ApplyCompletionCodeActionCommand(this.client));
		commandManager.register(new CompositeCommand());
		commandManager.register(new CompletionAcceptedCommand(onCompletionAccepted));
		commandManager.register(new 输入法上屏命令_());
	}

	public async provideCompletionItems(
		document: vscode.TextDocument,
		position: vscode.Position,
		token: vscode.CancellationToken,
		context: vscode.CompletionContext
	): Promise<vscode.CompletionList | null> {
		if (this.typingsStatus.isAcquiringTypings) {
			return Promise.reject<vscode.CompletionList>({
				label: localize(
					{ key: "正在获取 typings...", comment: ['类型定义是指 .d.cts 文件, 提高智能感知准确性, 不应该本地化'] },
					'Acquiring typings...'),
				detail: localize(
					{ key: "获取 智能感知 的 typings 定义。", comment: ['类型定义是指 .d.cts 文件, 提高智能感知准确性, 不应该本地化'] },
					'获取 智能感知 的 typings 定义。')
			});
		}

		const file = this.client.toOpenedFilePath(document);
		if (!file) {
			return null;
		}

		const line = document.lineAt(position.line);
		const 行前文本 = line.text.slice(0, position.character);
		const completionConfiguration = CompletionConfiguration.getConfigurationForResource(this.modeId, document.uri);

		if (!this.shouldTrigger(context, line, position)) {
			return null;
		}

		await this.client.interruptGetErr(() => this.fileConfigurationManager.ensureConfigurationForDocument(document, token));

		const args: Proto.CompletionsRequestArgs = {
			...typeConverters.Position.toFileLocationRequestArgs(file, position),
			includeExternalModuleExports: completionConfiguration.autoImportSuggestions,
			includeInsertTextCompletions: true,
			triggerCharacter: this.getTsTriggerCharacter(context),
		};

		let isNewIdentifierLocation = true;
		let isIncomplete = false;
		let isMemberCompletion = false;
		let dotAccessorContext: DotAccessorContext | undefined;
		let entries: ReadonlyArray<Proto.CompletionEntry>;
		let metadata: any | undefined;
		let response: ServerResponse.Response<Proto.CompletionsResponse | Proto.CompletionInfoResponse>
		if (this.client.apiVersion.gte(API.v300)) {
			response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token));
			if (response.type === 'response' && response.body) {
				isNewIdentifierLocation = response.body.isNewIdentifierLocation;
				isMemberCompletion = response.body.isMemberCompletion;
				if (isMemberCompletion) {
					const dotMatch = line.text.slice(0, position.character).match(/\.\s*$/) || undefined;
					if (dotMatch) {
						const range = new vscode.Range(position.translate({ characterDelta: -dotMatch[0].length }), position);
						const text = document.getText(range);
						dotAccessorContext = { range, text };
					}
				}
				isIncomplete = (response as any).metadata && (response as any).metadata.isIncomplete;
				entries = response.body.entries;
				metadata = response.metadata;
			}
		} else {
			response = await this.client.interruptGetErr(() => this.client.execute('completions', args, token));
			if (response.type == 'response' && response.body) {
				entries = response.body;
				metadata = response.metadata;
			}
		}
		if (response.type === "cancelled") {
			return null;
		}
		const isInValidCommitCharacterContext = this.isInValidCommitCharacterContext(document, position);

		if (response.type === "noContent" || !response.body) {
			return await 执行中文输入法命令(document, position, line, { isNewIdentifierLocation, isMemberCompletion, dotAccessorContext, isInValidCommitCharacterContext, enableCallCompletions: !completionConfiguration.useCodeSnippetsOnMethodSuggest }, isIncomplete);
		}

		const 中文条目组: Proto.CompletionEntry[] = [];
		entries = entries!.filter(entry => !shouldExcludeCompletionEntry(entry, completionConfiguration));

		for (let i = 0; i < entries.length; i++) {
			const entry = entries[i];
			if (中文标识符缓存.has(entry.name)) {
				entry.是库内中文结果 = true;
				entry.过滤文本 = 中文标识符缓存.get(entry.name);
			}
			else if (是中文标识符(entry.name)) {
				中文条目组.push(entry);
				entry.是库内中文结果 = true;
			}
			if (!关键字.has(entry.name)) {
				entry.范围 = 计算范围(行前文本, position);
			}
			else {
				entry.忽略上屏 = true;
			}
		}
		if (中文条目组 && 中文条目组.length) {
			let 拼音对象 = await vscode.commands.executeCommand<映射<string>>("vsc.取汉字拼音", 中文条目组.map(v => v.name));
			中文条目组.forEach(v => {
				let 拼音 = 拼音对象![v.name];
				if (拼音) {
					中文标识符缓存.set(v.name, 拼音);
					v.过滤文本 = 拼音;
				}
			});
		}

		const items = entries.map(entry => new MyCompletionItem(position, document, line.text, entry, completionConfiguration.useCodeSnippetsOnMethodSuggest, {
			isNewIdentifierLocation,
			isMemberCompletion,
			dotAccessorContext,
			isInValidCommitCharacterContext,
			enableCallCompletions: !completionConfiguration.useCodeSnippetsOnMethodSuggest
		}, metadata));

		return await 执行中文输入法命令(document, position, line, { isNewIdentifierLocation, isMemberCompletion, dotAccessorContext, isInValidCommitCharacterContext, enableCallCompletions: !completionConfiguration.useCodeSnippetsOnMethodSuggest }, isIncomplete, items);

	}
	private getTsTriggerCharacter(context: vscode.CompletionContext): Proto.CompletionsTriggerCharacter | undefined {
		// Workaround for https://github.com/Microsoft/TypeScript/issues/27321
		if (context.triggerCharacter === '@'
			&& this.client.apiVersion.gte(API.v310) && this.client.apiVersion.lt(API.v320)
		) {
			return undefined;
		}
		if (context.triggerCharacter && TypeScriptCompletionItemProvider.triggerCharacters.includes(context.triggerCharacter)) {
			return context.triggerCharacter as Proto.CompletionsTriggerCharacter;
		}
		return undefined;
	}

	public async resolveCompletionItem(
		item: vscode.CompletionItem,
		token: vscode.CancellationToken
	): Promise<vscode.CompletionItem | undefined> {
		if (!(item instanceof MyCompletionItem)) {
			return undefined;
		}
		let 上屏命令: vscode.Command;
		if ((<MyCompletionItem>item).是中文) {
			const 长度 = 取上屏字符长度(item);
			if (item.是输入法结果) {
				item.command = {
					title: "输入法上屏",
					command: "ctsscript.输入法上屏命令_",
					arguments: [undefined, new vscode.Range(new vscode.Position(item.range!.start.line, item.range!.start.character + 长度), new vscode.Position(item.range!.start.line, item.range!.start.character + 长度 + 1))]
				}
				return item;
			}
			else if (!item.忽略上屏) {
				上屏命令 = {
					title: "输入法上屏",
					command: "ctsscript.输入法上屏命令_",
					arguments: [item.label, new vscode.Range(new vscode.Position(item.range!.start.line, item.range!.start.character + 长度), new vscode.Position(item.range!.start.line, item.range!.start.character + 长度 + 1))]
				};
			}
		}

		const filepath = this.client.toOpenedFilePath(item.document);
		if (!filepath) {
			return undefined;
		}

		const args: Proto.CompletionDetailsRequestArgs = {
			...typeConverters.Position.toFileLocationRequestArgs(filepath, item.position),
			entryNames: [
				item.tsEntry.source ? { name: item.tsEntry.name, source: item.tsEntry.source } : item.tsEntry.name
			]
		};

		const response = await this.client.interruptGetErr(() => this.client.execute('completionEntryDetails', args, token));
		if (response.type !== 'response' || !response.body || !response.body.length) {
			return item;
		}

		const detail = response.body[0];

		if (!item.detail && detail.displayParts.length) {
			item.detail = Previewer.plain(detail.displayParts);
		}
		item.documentation = this.getDocumentation(detail, item);

		const codeAction = this.getCodeActions(detail, filepath);
		const commands: vscode.Command[] = [{
			command: CompletionAcceptedCommand.ID,
			title: '',
			arguments: [item]
		}];
		if (codeAction.command) {
			commands.push(codeAction.command);
		}
		if (上屏命令!) {
			commands.push(上屏命令!);
		}

		item.additionalTextEdits = codeAction.additionalTextEdits;

		if (detail && item.useCodeSnippet) {
			const shouldCompleteFunction = await this.isValidFunctionCompletionContext(filepath, item.position, item.document, token);
			if (shouldCompleteFunction) {
				const { snippet, parameterCount } = snippetForFunctionCall(item, detail.displayParts);
				item.insertText = snippet;
				if (parameterCount > 0) {
					commands.push({ title: 'triggerParameterHints', command: 'editor.action.triggerParameterHints' });
				}
			}
		}

		if (commands.length) {
			if (commands.length === 1) {
				item.command = commands[0];
			} else {
				item.command = {
					command: CompositeCommand.ID,
					title: '',
					arguments: commands
				};
			}
		}

		return item;
	}

	private getCodeActions(
		detail: Proto.CompletionEntryDetails,
		filepath: string
	): { command?: vscode.Command, additionalTextEdits?: vscode.TextEdit[] } {
		if (!detail.codeActions || !detail.codeActions.length) {
			return {};
		}

		// Try to extract out the additionalTextEdits for the current file.
		// Also check if we still have to apply other workspace edits and commands
		// using a vscode command
		const additionalTextEdits: vscode.TextEdit[] = [];
		let hasReaminingCommandsOrEdits = false;
		for (const tsAction of detail.codeActions) {
			if (tsAction.commands) {
				hasReaminingCommandsOrEdits = true;
			}

			// Apply all edits in the current file using `additionalTextEdits`
			if (tsAction.changes) {
				for (const change of tsAction.changes) {
					if (change.fileName === filepath) {
						additionalTextEdits.push(...change.textChanges.map(typeConverters.TextEdit.fromCodeEdit));
					} else {
						hasReaminingCommandsOrEdits = true;
					}
				}
			}
		}

		let command: vscode.Command | undefined = undefined;
		if (hasReaminingCommandsOrEdits) {
			// Create command that applies all edits not in the current file.
			command = {
				title: '',
				command: ApplyCompletionCodeActionCommand.ID,
				arguments: [filepath, detail.codeActions.map((x): Proto.CodeAction => ({
					commands: x.commands,
					description: x.description,
					changes: x.changes.filter(x => x.fileName !== filepath)
				}))]
			};
		}

		return {
			command,
			additionalTextEdits: additionalTextEdits.length ? additionalTextEdits : undefined
		};
	}

	private isInValidCommitCharacterContext(
		document: vscode.TextDocument,
		position: vscode.Position
	): boolean {
		if (this.client.apiVersion.lt(API.v320)) {
			// Workaround for https://github.com/Microsoft/TypeScript/issues/27742
			// Only enable dot completions when previous character not a dot preceeded by whitespace.
			// Prevents incorrectly completing while typing spread operators.
			if (position.character > 1) {
				const preText = document.getText(new vscode.Range(
					position.line, 0,
					position.line, position.character));
				return preText.match(/(\s|^)\.$/ig) === null;
			}
		}
		return true;
	}

	private shouldTrigger(
		context: vscode.CompletionContext,
		line: vscode.TextLine,
		position: vscode.Position
	): boolean {
		if (context.triggerCharacter && this.client.apiVersion.lt(API.v290)) {
			if ((context.triggerCharacter === '"' || context.triggerCharacter === '\'')) {
				// make sure we are in something that looks like the start of an import
				const pre = line.text.slice(0, position.character);
				if ((!pre.match(/\b(from|import)\s*["']$/) || !pre.match(/\b(来自|导入)\s*["']$/)) && (!pre.match(/\b(import|require)\(['"]$/) || !pre.match(/\b(导入|需要)\(['"]$/))) {
					return false;
				}
			}

			if (context.triggerCharacter === '/') {
				// make sure we are in something that looks like an import path
				const pre = line.text.slice(0, position.character);
				if ((!pre.match(/\b(from|import)\s*["'][^'"]*$/) || !pre.match(/\b(来自|导入)\s*["'][^'"]*$/)) && (!pre.match(/\b(import|require)\(['"][^'"]*$/) || !pre.match(/\b(导入|需要)\(['"][^'"]*$/))) {
					return false;
				}
			}

			if (context.triggerCharacter === '@') {
				// make sure we are in something that looks like the start of a jsdoc comment
				const pre = line.text.slice(0, position.character);
				if (!pre.match(/^\s*\*[ ]?@/) && !pre.match(/\/\*\*+[ ]?@/)) {
					return false;
				}
			}

			if (context.triggerCharacter === '<') {
				return false;
			}
		}

		return true;
	}

	private getDocumentation(
		detail: Proto.CompletionEntryDetails,
		item: MyCompletionItem
	): vscode.MarkdownString | undefined {
		const documentation = new vscode.MarkdownString();
		if (detail.source) {
			const importPath = `'${Previewer.plain(detail.source)}'`;
			const autoImportLabel = `从 ${importPath} 自动导入`;
			item.detail = `${autoImportLabel}\n${item.detail}`;
		}
		Previewer.addMarkdownDocumentation(documentation, detail.documentation, detail.tags);

		return documentation.value.length ? documentation : undefined;
	}

	private async isValidFunctionCompletionContext(
		filepath: string,
		position: vscode.Position,
		document: vscode.TextDocument,
		token: vscode.CancellationToken
	): Promise<boolean> {
		// Workaround for https://github.com/Microsoft/TypeScript/issues/12677
		// Don't complete function calls inside of destructive assigments or imports
		try {
			const args: Proto.FileLocationRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
			const response = await this.client.execute('quickinfo', args, token);
			if (response.type === 'response' && response.body) {
				switch (response.body.kind) {
					case '定义':
					case '变量':
					case '常量':
					case '别名':
						return false;
				}
			}
		}
		catch {
			// Noop
		}

		// Don't complete function call if there is already something that looks like a function call
		// https://github.com/Microsoft/vscode/issues/18131
		const after = document.lineAt(position.line).text.slice(position.character);
		return after.match(/^[a-z_$0-9]*\s*\(/gi) === null;
	}
}

function 取上屏字符长度(item: MyCompletionItem): number {
	if (item.insertText) {
		return typeof item.insertText === "string" ? item.insertText.length : item.insertText!.value.length;
	}
	return item.label.length;
}

async function 执行中文输入法命令(document: vscode.TextDocument, position: vscode.Position, line: vscode.TextLine, completionContext: CompletionContext, isIncomplete: boolean, items?: MyCompletionItem[]) {
	const lineText = line.text.slice(0, position.character);
	let { 开启中文提示, 已经输入的, 输入的拼音, 计算范围 } = 计算启动条件(lineText, position);
	if (开启中文提示) {
		const 库内词组 = await vscode.commands.executeCommand<库内词[]>("vsc.拼音输入法", 输入的拼音!.toLowerCase());
		const MyItems: MyCompletionItem[] = items || [];
		if (库内词组 && 库内词组.length) {
			for (let i = 0; i < 库内词组.length; i++) {
				const 词 = 库内词组[i];
				const 条目: Proto.CompletionEntry = <any>{
					name: `${i + 1}:${词.文本}`,
					插入文本: `${已经输入的}${词.文本}`,
					是输入法结果: true,
					过滤文本: `${已经输入的}${词.拼音}${词.剩余输入 ? 词.剩余输入 : ""}${i + 1}`,
					范围: 计算范围,
					kindModifiers: "",
					sortText: `0`,
					kind: PConst.Kind.script,
				}
				const 项目 = new MyCompletionItem(position, document, line.text, 条目, false, completionContext, undefined);
				MyItems.push(项目);
			}
		}
		return new vscode.CompletionList(MyItems, isIncomplete);
	}
	return items ? new vscode.CompletionList(items, isIncomplete) : null;
}

function 分割输入(文本: string) {
	let 匹配 = 文本.match(输入匹配);
	if (匹配 && 匹配.length) {
		let 已经输入的 = 匹配[1];
		let 输入的拼音 = 匹配[4];
		return { 输入的拼音, 已经输入的 };
	}
	return {};
}

function 计算范围(目前输入的部分: string, 位置: vscode.Position) {
	let { 输入的拼音, 已经输入的 } = 分割输入(目前输入的部分);
	if (输入的拼音) {
		let 替换起始位置 = 位置.character - 输入的拼音.length - (已经输入的 ? 已经输入的.length : 0);
		let 替换结束位置 = 位置.character;
		return new vscode.Range(new vscode.Position(位置.line, 替换起始位置!), new vscode.Position(位置.line, 替换结束位置!));
	}
	return undefined;
}

function 计算启动条件(lineText: string, 位置: vscode.Position) {
	let { 输入的拼音, 已经输入的 } = 分割输入(lineText);
	if (输入的拼音) {
		return { 开启中文提示: true, 已经输入的, 输入的拼音, 计算范围: 计算范围(lineText, 位置) };
	}
	return { 开启中文提示: false, 已经输入的: null, 输入的拼音: null, 计算范围: null };
}

function shouldExcludeCompletionEntry(
	element: Proto.CompletionEntry,
	completionConfiguration: CompletionConfiguration
) {
	return (
		(!completionConfiguration.nameSuggestions && element.kind === PConst.Kind.warning)
		|| (!completionConfiguration.pathSuggestions &&
			(element.kind === PConst.Kind.directory || element.kind === PConst.Kind.script || element.kind === PConst.Kind.externalModuleName))
		|| (!completionConfiguration.autoImportSuggestions && element.hasAction)
	);
}

export function register(
	selector: vscode.DocumentSelector,
	modeId: string,
	client: ITypeScriptServiceClient,
	typingsStatus: TypingsStatus,
	fileConfigurationManager: FileConfigurationManager,
	commandManager: CommandManager,
	onCompletionAccepted: (item: vscode.CompletionItem) => void
) {
	return new ConfigurationDependentRegistration(modeId, 'suggest.enabled', () =>
		vscode.languages.registerCompletionItemProvider(selector,
			new TypeScriptCompletionItemProvider(client, modeId, typingsStatus, fileConfigurationManager, commandManager, onCompletionAccepted),
			...TypeScriptCompletionItemProvider.triggerCharacters, ...TypeScriptCompletionItemProvider.triggerCharactersCts));
}
